package android.webkit;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import android.content.Context;
import android.net.http.Headers;
import android.util.Log;

// Referenced classes of package android.webkit:
//            CacheSyncManager, HttpDateTime, URLUtil

public final class CacheManager1
{
  private static final String httpString = "http://";
  private static final String cacheDirName = "browserCache";
  private static String cacheDir = null;

  static class CacheResult
  {

      ByteArrayOutputStream stream;
      String lastModified;
      String etag;
      long localPath;
      long expires;
      String mimeType;
      String encoding;
      long updateTime;
      int hitCount;
      int hitCountWhileDeleted;

      CacheResult()
      {
      }
  }


  public CacheManager1()
  {
  }

  public static void setCacheDisabled(boolean disabled)
  {
      if(disabled == mDisabled)
          return;
      mDisabled = disabled;
      if(mDisabled)
          removeAllCacheFiles();
  }

  public static boolean cacheDisabled()
  {
      return mDisabled;
  }

  static Map getCacheMap()
  {
      return mUrlIds;
  }

  static boolean needsSync()
  {
      Map map = mUrlIds;
//        JVM INSTR monitorenter ;
//        return mNeedsSync;
//        Exception exception;
//        exception;
//        throw exception;

      return mNeedsSync;
  }

  static void onDoneSync()
  {
      synchronized(mUrlIds)
      {
          mNeedsSync = false;
      }
  }

  static CacheResult getCacheFile(String url, Map headers)
  {
      logD("get cache file: " + url);
      if(skipCache(url))
          return null;
      CacheResult result;
      synchronized(mUrlIds)
      {
          result = (CacheResult)mUrlIds.get(url);
      }
      if(result == null)
          return null;
      if(headers != null && result.expires >= 0L && result.expires <= System.currentTimeMillis())
      {
          if(result.lastModified == null && result.etag == null)
              return null;
          if(result.etag != null)
              headers.put("if-none-match", result.etag);
          if(result.lastModified != null)
              headers.put("if-modified-since", result.lastModified);
      }
      return result;
  }

  static CacheResult createCacheFile(String url, Headers headers)
  {
      logD("creating cache file: " + url);
      if(skipCache(url))
          return null;
      CacheResult ret = parseHeaders(headers);
      if(ret != null)
      {
          int hashCode = url.substring(url.indexOf(File.separatorChar) + 2).hashCode();
          ret.localPath = hashCode != -2147483648 ? Math.abs(hashCode) : 2147483647L;
          ret.stream = new ByteArrayOutputStream();
      }
      return ret;
  }

  static void saveCacheFile(String url, CacheResult cacheRet, boolean fromDatabase)
  {
      logD("saving file: " + url);
      if(cacheRet.stream.size() > sCacheMaxObjectSize)
          return;
      if(!fromDatabase)
          cacheRet.updateTime = System.currentTimeMillis();
      makeRoom();
      CacheResult old = null;
      synchronized(mUrlIds)
      {
          if(fromDatabase)
          {
              if(mUrlIds.get(url) == null)
                  mUrlIds.put(url, cacheRet);
          } else
          {
              old = (CacheResult)mUrlIds.put(url, cacheRet);
              mNeedsSync = true;
          }
          mCacheTotalSize += cacheRet.stream.size();
      }
      if(old != null)
          mCacheTotalSize -= old.stream.size();
  }

  static boolean removeAllCacheFiles()
  {
      logD("removing all cache files");
      synchronized(mUrlIds)
      {
          mUrlIds.clear();
          mCacheTotalSize = 0;
      }
      CacheSyncManager.getInstance().removeAll();
      return true;
  }

  public static boolean cacheEmpty()
  {
      return mCacheTotalSize == 0;
  }

  private static CacheResult parseHeaders(Headers headers)
  {
      logD("parsing headers");
      String length = headers.getValue(1);
      if(length != null)
          try
          {
              if(Long.parseLong(length) > (long)sCacheMaxObjectSize)
                  return null;
          }
          catch(NumberFormatException ex) { }
      CacheResult ret = new CacheResult();
      ret.expires = -1L;
      String expires = headers.getValue(7);
      if(expires != null)
          try
          {
              ret.expires = HttpDateTime.parse(expires).longValue();
          }
          catch(IllegalArgumentException ex)
          {
              Log.e("cache", (new StringBuilder()).append("illegal expires: ").append(expires).toString());
          }
      String lastModified = headers.getValue(9);
      if(lastModified != null)
          ret.lastModified = lastModified;
      String etag = headers.getValue(10);
      if(etag != null)
          ret.etag = etag;
      String cacheControl = headers.getValue(8);
      if(cacheControl != null)
      {
          String controls[] = cacheControl.toLowerCase().split("[ ,;]");
          for(int i = 0; i < controls.length; i++)
          {
              if("no-store".equals(controls[i]))
                  return null;
              if("no-cache".equals(controls[i]))
              {
                  ret.expires = 0L;
                  continue;
              }
              if(!controls[i].startsWith("max-age"))
                  continue;
              int separator = controls[i].indexOf('=');
              if(separator < 0)
                  separator = controls[i].indexOf(':');
              if(separator <= 0)
                  continue;
              try
              {
                  long sec = Long.parseLong(controls[i].substring(separator + 1));
                  if(sec > 1L)
                      ret.expires = System.currentTimeMillis() + 1000L * sec;
                  continue;
              }
              catch(NumberFormatException ex)
              {
                  Log.e("cache", (new StringBuilder()).append("exception in parseHeaders for max-age:").append(controls[i].substring(separator + 1)).toString());
              }
              ret.expires = 0L;
          }

      }
      if(ret.expires == -1L)
          if(ret.lastModified == null)
          {
              ret.expires = System.currentTimeMillis() + 86400000L;
          } else
          {
              long lastmod = System.currentTimeMillis() + 86400000L;
              try
              {
                  lastmod = HttpDateTime.parse(ret.lastModified).longValue();
              }
              catch(IllegalArgumentException ex)
              {
                  Log.e("cache", (new StringBuilder()).append("illegal lastModified: ").append(ret.lastModified).toString());
              }
              long difference = System.currentTimeMillis() - lastmod;
              if(difference > 0L)
                  ret.expires = System.currentTimeMillis() + difference / 5L;
              else
                  ret.expires = lastmod;
          }
      return ret;
  }

  private static void makeRoom()
  {
      logD("make room");
      if(mCacheTotalSize > mCacheMaxTotalSize || mUrlIds.size() == mCacheMaxFileCount)
          synchronized(mUrlIds)
          {
              long toStay = Math.round(0.90000000000000002D * (double)mUrlIds.size());
              Set set = mUrlIds.entrySet();
              for(Iterator iter = set.iterator(); iter.hasNext();)
                  if(toStay > 0L)
                  {
                      toStay--;
                      iter.next();
                  } else
                  {
                      java.util.Map.Entry entry = (java.util.Map.Entry)iter.next();
                      CacheResult ret = (CacheResult)entry.getValue();
                      mCacheTotalSize -= ret.stream.size();
                      iter.remove();
                  }

          }
  }

  private static boolean skipCache(String url)
  {
      if(mDisabled)
          return true;
      boolean skip = !URLUtil.isNetworkUrl(url);
      if (skip)
        logD("skipping cache file: " + url);
      return skip;
  }

  private static void report()
  {
      int total[] = new int[13];
      int hitall[] = new int[13];
      int hit[] = new int[13];
      int totalSize = 0;
      Set set = mUrlIds.entrySet();
      for(Iterator iter = set.iterator(); iter.hasNext();)
      {
          java.util.Map.Entry map = (java.util.Map.Entry)iter.next();
          CacheResult ret = (CacheResult)map.getValue();
          addToBucket(ret, total, hitall, hit);
          totalSize += ret.stream.size();
      }

      set = mDeletedUrls.entrySet();
      for(Iterator iter = set.iterator(); iter.hasNext();)
      {
          java.util.Map.Entry map = (java.util.Map.Entry)iter.next();
          CacheResult ret = (CacheResult)map.getValue();
          addToBucket(ret, total, hitall, hit);
          totalSize += ret.stream.size();
      }

      Log.w("cache", (new StringBuilder()).append("cache hit ").append(mHitCount).append(" and ").append(mNeedValidationCount).append(" needValidate, miss ").append(mMissCount).append(" and ").append(mExpiredCount).append(" expired.").toString());
      Log.w("cache", (new StringBuilder()).append("current cache entries: ").append(mUrlIds.size()).append("; deleted cache entries: ").append(mDeletedUrls.size()).toString());
      Log.w("cache", (new StringBuilder()).append("current cache size: ").append(mCacheTotalSize).append("; total cahe size: ").append(totalSize).toString());
      Log.w("cache", (new StringBuilder()).append("total map:\n<256: ").append(total[0]).append("; 256:").append(total[1]).append("; 512:").append(total[2]).append("; 1k:").append(total[3]).append("; 2k:").append(total[4]).append("; 4k:").append(total[5]).append("; 8k:").append(total[6]).append("; 16k:").append(total[7]).append("; 32k:").append(total[8]).append("; 64k:").append(total[9]).append("; 128k:").append(total[10]).append("; 256k:").append(total[11]).append("; >256k:").append(total[12]).toString());
      Log.w("cache", (new StringBuilder()).append("potential hit map:\n<256: ").append(hitall[0]).append("; 256:").append(hitall[1]).append("; 512:").append(hitall[2]).append("; 1k:").append(hitall[3]).append("; 2k:").append(hitall[4]).append("; 4k:").append(hitall[5]).append("; 8k:").append(hitall[6]).append("; 16k:").append(hitall[7]).append("; 32k:").append(hitall[8]).append("; 64k:").append(hitall[9]).append("; 128k:").append(hitall[10]).append("; 256k:").append(hitall[11]).append("; >256k:").append(hitall[12]).toString());
      Log.w("cache", (new StringBuilder()).append("real hit map:\n<256: ").append(hit[0]).append("; 256:").append(hit[1]).append("; 512:").append(hit[2]).append("; 1k:").append(hit[3]).append("; 2k:").append(hit[4]).append("; 4k:").append(hit[5]).append("; 8k:").append(hit[6]).append("; 16k:").append(hit[7]).append("; 32k:").append(hit[8]).append("; 64k:").append(hit[9]).append("; 128k:").append(hit[10]).append("; 256k:").append(hit[11]).append("; >256k:").append(hit[12]).toString());
      mHitCount = 0;
      mMissCount = 0;
      mExpiredCount = 0;
      mNeedValidationCount = 0;
      mDeletedUrls.clear();
  }

  private static void addToBucket(CacheResult ret, int total[], int hitall[], int hit[])
  {
      int index = 0;
      int base = 256;
      do
      {
          if(ret.stream.size() <= base)
              break;
          index++;
          base *= 2;
      } while(base <= 262144);
      total[index] += ret.hitCount + ret.hitCountWhileDeleted + 1;
      hitall[index] += ret.hitCount + ret.hitCountWhileDeleted;
      hit[index] += ret.hitCount;
  }

  private static final String LOGTAG = "cache";
  static final String HEADER_KEY_IFMODIFIEDSINCE = "if-modified-since";
  static final String HEADER_KEY_IFNONEMATCH = "if-none-match";
  static int sCacheMaxObjectSize = 262144;
  private static final String NO_STORE = "no-store";
  private static final String NO_CACHE = "no-cache";
  private static final String MAX_AGE = "max-age";
  private static int mCacheMaxTotalSize = 2097152;
  private static int mCacheMaxFileCount;
  private static Map mUrlIds;
  private static int mCacheTotalSize;
  private static boolean mDisabled;
  private static boolean mNeedsSync;
  private static final boolean mDoReport = false;
  private static int mHitCount;
  private static int mMissCount;
  private static int mExpiredCount;
  private static int mNeedValidationCount;
  private static Map mDeletedUrls = new HashMap();

  static
  {
      mCacheMaxFileCount = 1000;
      mUrlIds = new LinkedHashMap(mCacheMaxFileCount, 0.75F, true);
  }

  public static void logD(String s) {
    Log.d("cache", s);
  }

  public static String[] getCache(Context con, String hostUrl) {
    WebViewDatabase wvd = WebViewDatabase.getInstance(con);
    ArrayList<String> s = new ArrayList<String>();
    ArrayList<CacheManager.CacheResult> c = new ArrayList<CacheManager.CacheResult>();
    wvd.getAllCacheResults(s, c);
    ArrayList<String> cacheForHost = new ArrayList<String>();
    for (int i = 0; i < s.size(); i++) {
      CacheManager.CacheResult cr = c.get(i);
      String url = s.get(i);
      String crString = url + "," + cr.expires + "," + cr.lastModified + "," + cr.etag;
      if (url.toLowerCase().contains(crString.toLowerCase()))
        cacheForHost.add(crString);
    }

    return cacheForHost.toArray(new String[0]);
  }

  public static void clearCache(Context con) {
    logD("clearing cache");
    if (cacheDir == null)
      setCacheDir(con);

    WebViewDatabase w = WebViewDatabase.getInstance(con);
    w.clearCache();
    File dir = con.getDir(cacheDirName, 0);
    deleteDir(dir);
    dir.mkdir();
  }

  private static void setCacheDir(Context con) {
    File file = con.getDir(cacheDirName, 0);
    file.mkdirs();
    cacheDir = file.toString();
  }

  private static boolean deleteDir(File dir) {
    if (dir.isDirectory()) {
      String[] children = dir.list();
      for (int i=0; i<children.length; i++) {
        boolean success = deleteDir(new File(dir, children[i]));
        if (!success) {
          return false;
        }
      }
    }

    // The directory is now empty so delete it
    return dir.delete();
  }
  
  private static byte[] getDataFromStream(InputStream is) {
    BufferedReader reader = null;
    try {
      reader = new BufferedReader(new InputStreamReader(is), 8000);
      ByteArrayOutputStream b = new ByteArrayOutputStream();
      int oneByte;
      while ((oneByte = reader.read()) != -1) {
        b.write(oneByte);
      }
      return b.toByteArray();
    } catch (FileNotFoundException f) {
      logD("file not found: " + f.getMessage());
      return null;
    } catch (IOException i) {
      logD("couldn't read file: " + i.getMessage());
      return null;
    } finally {
      try {
        if (reader != null)
          reader.close();
      } catch (IOException i) {}
    }
  }

  private static void writeToDiskAndDBase(Context con, String url, long localPath, byte[] data, CacheManager.CacheResult ret) {
    PrintWriter writer = null;
    if (data == null) {
      logD("data is null!");
      return;
    }

    if (cacheDir == null)
      setCacheDir(con);

    FileOutputStream fos = null;
    try {
      File file = new File(cacheDir, Long.valueOf(localPath).toString());
      if (!file.exists())
        file.createNewFile();
      fos = new FileOutputStream(file);
      fos.write(data);
    } catch (FileNotFoundException f) {
      logD("file not found: " + f.getMessage());
      return;
    } catch (IOException i) {
      logD("couldn't create file");
      return;
    } finally {
      if (writer != null)
        writer.close();
      if (fos != null) {
        try {
          fos.close();
        } catch (IOException i) {}
      }
    }

    if (ret == null)
      return;

    WebViewDatabase wvd = WebViewDatabase.getInstance(con);
    wvd.addCache(url, ret);
    logD("put file in cache");
  }

//  public static void delete(Context con, String url) {
//    long localPath = getLocalName(con, url);
//    WebViewDatabase w = WebViewDatabase.getInstance(con);
//    ArrayList<String> urls = new ArrayList<String>();
//    urls.add(url);
//    w.removeUnreferenceCache(urls);
//    if (cacheDir == null)
//      setCacheDir(con);
//
//    File file = new File(cacheDir, Long.valueOf(localPath).toString());
//    if (file.exists())
//      file.delete();
//  }

  public static byte[] get(Context con, String url) {
    logD("cache get");
    long localPath = getLocalName(con, url);
    if (cacheDir == null)
      setCacheDir(con);

    File file = new File(cacheDir, Long.valueOf(localPath).toString());
    if (!file.exists())
      return null;

    FileInputStream fis = null;
    try {
      fis = new FileInputStream(file);
      return getDataFromStream(fis);
    } catch (FileNotFoundException f) {
      return null;
    } finally {
      try {
        if (fis != null)
          fis.close();
      } catch (IOException i) {}
    }
  }

  public static void put(Context con, String urlStr, byte[] data, long expires) {
    CacheManager.CacheResult cr = new CacheManager.CacheResult();
    cr.lastModified = new Date(System.currentTimeMillis()).toString();
    cr.expires = expires;

    long localPath = getLocalName(con, urlStr);
    cr.localPath = localPath;
    writeToDiskAndDBase(con, urlStr, localPath, data, cr);
  }

  public static void put(Context con, String urlStr, byte[] data) {
    long localPath = getLocalName(con, urlStr);
    writeToDiskAndDBase(con, urlStr, localPath, data, null);
  }

  public static void put(Context con, String urlStr) {
    HttpURLConnection conn = null;
    CacheManager.CacheResult cr = new CacheManager.CacheResult();
    byte[] data = null;
    try {
      URL url = new URL(urlStr);
      conn = (HttpURLConnection)url.openConnection();
      conn.setDoInput(true);
      conn.setRequestMethod("GET");
      conn.connect();
      data = getDataFromStream(conn.getInputStream());
      if (data == null)
        return;
      cr.etag = conn.getHeaderField("ETag");
      cr.lastModified = conn.getHeaderField("Last-Modified");
      String s = conn.getHeaderField("Expires");
      Long millis = null;
      if ((millis = Long.getLong(s)) != null)
        cr.expires = millis.longValue();
      else {
        SimpleDateFormat sdf = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z");
        Date d = sdf.parse(s);
        cr.expires = d == null ? -1L : d.getTime();
      }
    } catch (MalformedURLException m) {
      logD("couldn't put " + urlStr + ", " + m.getMessage());
      return;
    } catch (ProtocolException p) {
      logD("couldn't issue GET " + urlStr + ", " + p.getMessage());
      return;
    } catch (IOException i) {
      logD("couldn't connect to " + urlStr + ", " + i.getMessage());
      return;
    } catch (ParseException p) {
      logD("couldn't parse expires header " + ", " + p.getMessage());
      cr.expires = -1L;
    }  finally {
      if (conn != null)
        conn.disconnect();
    }

    if (data == null)
      return;

    long localPath = getLocalName(con, urlStr);
    cr.localPath = localPath;
    writeToDiskAndDBase(con, urlStr, localPath, data, cr);
  }

  public static long getLocalName(Context con, String url) {
    int hashCode = url.substring(url.indexOf(File.separatorChar) + 2).hashCode();
    long localPath = hashCode != 0x80000000 ? Math.abs(hashCode) : 0x7fffffffL;
    return localPath;
  }

  public static void printToLog(Context con, String domain) {
    WebViewDatabase wvd = WebViewDatabase.getInstance(con);
    ArrayList<String> s = new ArrayList<String>();
    ArrayList<CacheManager.CacheResult> c = new ArrayList<CacheManager.CacheResult>();
    wvd.getAllCacheResults(s, c);
    for (int i = 0; i < s.size(); i++) {
      Log.d("DBASE", "CR: " + s.get(i) + ": " + c.get(i).localPath);
    }
    if (domain.startsWith("http://"))
      domain = domain.substring(7);
    ArrayList<CookieManager.Cookie> cookies = wvd.getCookiesForDomain(domain);
    for (CookieManager.Cookie cookie: cookies) {
      Log.d("DBASE", "Cookie: " + cookie.value);
    }
  }
}